# 👉 webpack 构建流程
参考文章:
- 90 行代码的 webpack,你确定不学吗? (opens new window)
- 一文掌握 Webpack 编译流程 (opens new window)
- 揭秘 webpack 插件工作流程和原理 (opens new window)
- Webpack 源码解读:理清编译主流程 (opens new window)
- webpack 是如何实现动态导入的 (opens new window)
- 『Webpack 系列』—— 路由懒加载的原理 (opens new window)
还有,tecvan 大佬总结的非常有深度的 webpack 系列:
- [万字总结] 一文吃透 Webpack 核心原理 (opens new window)
- 有点难的 webpack 知识点:Dependency Graph 深度解析 (opens new window)
# webpack 做了什么
我们可以从构建文件作为开始,进行分析。
// index.js
const one = require("./one.js");
one();
// one.js
import("./two.js").then(({ two }) => {
console.log("import two");
two();
});
module.exports = function() {
console.log("one");
};
// two.js
module.exports = function() {
console.log("two");
};
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const CleanWebpackPlugin = require("clean-webpack-plugin");
module.exports = {
mode: "development",
entry: {
main: path.resolve(__dirname, "../src/index.js"),
},
output: {
filename: "js/[chunkhash].js",
path: path.resolve(__dirname, "../dist"),
},
plugins: [
new CleanWebpackPlugin(),
new HtmlWebpackPlugin({
title: "webpack-case",
template: path.resolve(__dirname, "../public/index.html"),
}),
],
};
最后执行webpack --config webpack.config.js
后,在dist/js
文件夹下创建了一个35f63560f4987ba7b874.js(对应主入口)和35f63560f4987ba7b874.js(一个对应动态引入的two.js)
在删除了一些注视符号和增加了一些帮助自己理解的注释后:
// e74980cea533ebbd6176.js
// self["webpackChunkwebpack_cases"]数组会push进一个数组,而这个数组是two.js的信息,其中第二项是正是表示源码的一个对象
(self["webpackChunkwebpack_cases"] =
self["webpackChunkwebpack_cases"] || []).push([
["src_two_js"],
{
"./src/two.js": (module) => {
eval(
"// two.js\nmodule.exports = function () {\n console.log('two')\n}\n\n//# sourceURL=webpack://webpack-cases/./src/two.js?"
);
},
},
]);
// 35f63560f4987ba7b874.js
// 创建一个自执行函数
(() => {
// webpackBootstrap
// index.js和one.js 被以键值对的形式保存到了 __webpack_modules__ 上
// 对象的 key 为模块路径名,value 为一个被包装过的模块函数。
// 函数拥有 module, module.exports, __webpack_require__ 三个参数。
// 这使得每个模块都拥有使用 module.exports 导出本模块和使用 __webpack_require__ 引入其他模块的能力,同时保证了每个模块都处于一个隔离的函数作用域范围。
var __webpack_modules__ = {
"./src/index.js": (
__unused_webpack_module,
__unused_webpack_exports,
__webpack_require__
) => {
eval(
"const one = __webpack_require__(/*! ./one.js */ \"./src/one.js\");\none();\n\n// class Animal {\n// constructor(name) {\n// this.name = name;\n// }\n// getName() {\n// return this.name;\n// }\n// }\n\n// const dog = new Animal('dog');\n\n//# sourceURL=webpack://webpack-cases/./src/index.js?"
);
},
"./src/one.js": (
module,
__unused_webpack_exports,
__webpack_require__
) => {
// 对于异步引入会被转换成 __webpack_require__.e
// __webpack_require__.e方法是实现懒加载的核心
eval(
"__webpack_require__.e(/*! import() */ \"src_two_js\").then(__webpack_require__.t.bind(__webpack_require__, /*! ./two.js */ \"./src/two.js\", 23)).then(({two}) => {\n console.log('import two');\n two();\n})\n\nmodule.exports = function () {\n console.log('one')\n}\n\n//# sourceURL=webpack://webpack-cases/./src/one.js?"
);
},
};
// The module cache
// 使用 __webpack_require__ 函数加载完成的模块会被缓存到 __webpack_module_cache__ 对象上,以便下次如果有其他模块依赖此模块时,不需要重新运行模块的包装函数,减少执行效率的消耗。同时,也避免了循环依赖无限递归的出现。
var __webpack_module_cache__ = {};
// 定义require函数
// require是node环境自带的环境变量,可以直接使用,而在其他环境则没有这样一个变量,于是需要webpack提供这样相似的能力,
function __webpack_require__(moduleId) {
// 检查是否已经构建过
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 新构建,则为它创建一个专属的moduleId并缓存起来
// Create a new module (and put it into the cache)
var module = (__webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {},
});
// 执行模块里的函数代码
// Execute the module function
__webpack_modules__[moduleId](
module,
module.exports,
__webpack_require__
);
// Return the exports of the module
return module.exports;
}
// 暴露modules expose the modules object (__webpack_modules__)
__webpack_require__.m = __webpack_modules__;
// ...
/* webpack/runtime/ensure chunk */
(() => {
__webpack_require__.f = {};
// This file contains only the entry chunk.
// The chunk loading function for additional chunks
__webpack_require__.e = (chunkId) => {
return Promise.all(
Object.keys(__webpack_require__.f).reduce((promises, key) => {
__webpack_require__.f[key](chunkId, promises);
return promises;
}, [])
);
};
})();
/* webpack/runtime/jsonp chunk loading */
(() => {
// object to store loaded and loading chunks
// undefined = chunk not loaded, null = chunk preloaded/prefetched
// [resolve, reject, Promise] = chunk loading, 0 = chunk loaded
var installedChunks = {
main: 0,
};
//...
// install a JSONP callback for chunk loading
// 执行 installedChunks 中的 resolve , 让 import() 得以继续执行。
// 将 chunk 中含有的模块全部注册到__webpack_require__.m变量中。
var webpackJsonpCallback = (parentChunkLoadingFunction, data) => {
var [chunkIds, moreModules, runtime] = data;
// add "moreModules" to the modules object,
// then flag all "chunkIds" as loaded and fire callback
var moduleId,
chunkId,
i = 0;
for (moduleId in moreModules) {
if (__webpack_require__.o(moreModules, moduleId)) {
__webpack_require__.m[moduleId] = moreModules[moduleId];
}
}
if (runtime) var result = runtime(__webpack_require__);
// 这句代码在多入口项目中才有作用,
if (parentChunkLoadingFunction) parentChunkLoadingFunction(data);
for (; i < chunkIds.length; i++) {
chunkId = chunkIds[i];
if (
__webpack_require__.o(installedChunks, chunkId) &&
installedChunks[chunkId]
) {
installedChunks[chunkId][0]();
}
installedChunks[chunkIds[i]] = 0;
}
};
// 先把self["webpackChunkwebpack_cases"]赋值给chunkLoadingGlobal
var chunkLoadingGlobal = (self["webpackChunkwebpack_cases"] =
self["webpackChunkwebpack_cases"] || []);
chunkLoadingGlobal.forEach(webpackJsonpCallback.bind(null, 0));
// 用webpackJsonpCallback函数拦截chunkLoadingGlobal的push方法
// 也就是说调用self["webpackChunkwebpack_cases"]的push方法都会执行webpackJsonpCallback函数。
chunkLoadingGlobal.push = webpackJsonpCallback.bind(
null,
chunkLoadingGlobal.push.bind(chunkLoadingGlobal)
);
})();
// ...
// 从入口开始,递归读取依赖模块
// startup
// Load entry module and return exports
// This entry module can't be inlined because the eval devtool is used.
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();
从以上来看,webpack 主要做的事情有:
- 读取入口文件,并收集依赖信息
- 递归地读取所有依赖模块,产出完整的依赖列表
- 将各模块内容打包成一块完整的可运行代码
其中涉及:
@babel/parser
用于解析源代码,产出 AST@babel/traverse
用于遍历 AST,找到 require 语句并修改成 require,将引入路径改造为相对根的路径@babel/core
用于将修改后的 AST 转换成新的代码输出
针对动态引入的模块, 会另外打包出一个 chunk,并且在源代码中通过__webpack_require__.e
引入,这个函数主要做的事情:
- 设置 chunk 加载的三种状态并缓存在 installedChunks 中,防止 chunk 重复加载:
- installedChunks[chunkId]为 0,代表该 chunk 已经加载完毕。
- installedChunks[chunkId]为 undefined,代表该 chunk 加载失败、加载超时、从未加载过。
- installedChunks[chunkId]为 Promise 对象,代表该 chunk 正在加载。
- 假如没加载过,则发起一个 JSONP 请求去加载 chunk
- 处理 chunk 加载超时和加载出错的场景。
# webpack 的构建流程
# 初始化阶段(compile)
# Step1:初始化参数
结合默认配置文件、配置对象和 shell 命令参数,得出最终的配置参数。
# Step2:创建编译器对象 Compiler
用配置参数创建编译器对象,Compiler 负责文件监听和启动编译。
Compiler 实例中包含了完整的 Webpack 配置,全局只有一个 Compiler 实例,Compiler 将贯穿整个 webpack 构建过程,直到运行结束。
# Step3:初始化编译环境
包括注入内置插件、注册各种模块工厂、初始化 RuleSet 集合、加载配置的插件等。
注入内置插件:
webpack 内置了数百个插件,这些插件并不需要我们手动配置,而是通过 WebpackOptionsApply 类会在初始化阶段根据配置内容动态注入对应的插件。其中包括:- EntryOptionPlugin 插件,处理 entry 配置;
- 根据 devtool 值判断后续用哪个插件处理 sourcemap;
- 注入 RuntimePlugin ,用于根据代码内容动态注入 webpack 运行时
- ...
加载插件:
会依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点。同时给插件传入 compiler 实例的引用,以方便插件通过 compiler 调用 Webpack 提供的 API。
# Step4:开始编译 compiler.run
相应的环境参数预设好了,接着执行 compiler 对象的 run 方法。
其中,run 方法会触发 compiler.compile,接着进一步。
这里会创建 compilation,compilation 代表一次单一的版本构建和生成资源。compilation 编译作业可以多次执行,比如 webpack 工作在 watch 模式下,每次监测到源文件发生变化时,都会重新实例化一个 compilation 对象。
一个 compilation 对象表现了当前的模块资源、编译生成资源和被跟踪依赖的状态信息。
# Step5:确认入口
根据配置中的 entry 找出所有的入口文件,调用 compilition.addEntry 将入口文件转换为 dependence 对象。
// 取自 lib/webpack.js:
// https://github.com/webpack/webpack/blob/2151f56ae7/lib/webpack.js#L57
// Step0: 启动 webpack ,触发 lib/webpack.js 文件中 createCompiler 方法
const webpack = (options, callback) => {
let compiler = createCompiler(options)
...
// 如果传入callback函数,则自启动
if(callback){
// Step4: 执行compiler.run / compiler.watch(watch模式下)
// 相关详细源码可见:https://github.com/webpack/webpack/blob/ad1c80214d30676edcf83cb8f74135646ced9cc8/lib/webpack.js#L133
// 在run函数中出现的钩子有:beforeRun --> run --> done --> afterDone,第三方插件可以钩住不同的生命周期,接收compiler对象,处理不同逻辑。
compiler.run((err, states) => {
// compiler.run方法会触发compiler.compile(和Step5有关,往下看)
compiler.close((err2)=>{
callbacl(err || err2, states)
})
})
}
...
return compiler
}
const createCompiler = (rawOptions) => {
// webpack内部会有一个默认的配置,在 webpack.js入口处理函数中,初始化了所有的默认配置。
// 主要有三种对模块的默认配置,输出output,解析optimization,加载resolve模块以及resolveLoader。
// Step1: getNormalizedWebpackOptions 和 applyWebpackOptionsBaseDefaults 合并出最终配置(WebpackOptionsDefaulter里的逻辑)
const options = getNormalizedWebpackOptions(rawOptions);
applyWebpackOptionsBaseDefaults(options);
// Step2: 用配置参数创建编译器对象,Compiler 实例中包含了完整的 Webpack 配置
const compiler = new Compiler(options.context);
compiler.options = options;
// 插件通过apply接受compiler参数
new NodeEnvironmentPlugin({
infrastructureLogging: options.infrastructureLogging,
}).apply(compiler);
if (Array.isArray(options.plugins)) {
// 依次调用插件的 apply 方法,让插件可以监听后续的所有事件节点
for (const plugin of options.plugins) {
if (typeof plugin === "function") {
plugin.call(compiler, compiler);
} else {
plugin.apply(compiler);
}
}
}
applyWebpackOptionsDefaults(options);
compiler.hooks.environment.call();
compiler.hooks.afterEnvironment.call();
// Step3:初始化编译环境,根据配置内容动态注入对应的插件
new WebpackOptionsApply().process(options, compiler);
compiler.hooks.initialize.call();
return compiler;
};
// 取自 lib/WebpackOptionsApply.js:
// https://github.com/webpack/webpack/blob/2151f56ae73b35722c56432c9b34820626d72e53/lib/WebpackOptionsApply.js#L278
// Step3-1: 调用 new WebpackOptionsApply().process 方法,注入各种内置插件
// EntryOptionPlugin内部的apply方法,会注入EntryPlugin,EntryPlugin 监听 compiler.make 钩子(和Step5有关)
new EntryOptionPlugin().apply(compiler);
compiler.hooks.entryOption.call(options.context, options.entry);
// 相关源码可见:
// EntryOptionPlugin:
// https://github.com/webpack/webpack/blob/2151f56ae73b35722c56432c9b34820626d72e53/lib/EntryOptionPlugin.js#L18
// EntryPlugin:
// https://github.com/webpack/webpack/blob/2151f56ae73b35722c56432c9b34820626d72e53/lib/EntryPlugin.js#L33
// Step3-2:注入 RuntimePlugin,用于根据代码内容动态注入 webpack 运行时
new RuntimePlugin().apply(compiler);
// Step3-3: 根据 devtool 值判断后续用那个插件处理 sourcemap
// EvalSourceMapDevToolPlugin 、SourceMapDevToolPlugin、EvalDevToolModulePlugin
if (options.devtool) {
if (options.devtool.includes("source-map")) {
const hidden = options.devtool.includes("hidden");
const inline = options.devtool.includes("inline");
const evalWrapped = options.devtool.includes("eval");
const cheap = options.devtool.includes("cheap");
const moduleMaps = options.devtool.includes("module");
const noSources = options.devtool.includes("nosources");
const Plugin = evalWrapped
? require("./EvalSourceMapDevToolPlugin")
: require("./SourceMapDevToolPlugin");
new Plugin({
filename: inline ? null : options.output.sourceMapFilename,
moduleFilenameTemplate:
options.output.devtoolModuleFilenameTemplate,
fallbackModuleFilenameTemplate:
options.output.devtoolFallbackModuleFilenameTemplate,
append: hidden ? false : undefined,
module: moduleMaps ? true : cheap ? false : true,
columns: cheap ? false : true,
noSources: noSources,
namespace: options.output.devtoolNamespace,
}).apply(compiler);
} else if (options.devtool.includes("eval")) {
const EvalDevToolModulePlugin = require("./EvalDevToolModulePlugin");
new EvalDevToolModulePlugin({
moduleFilenameTemplate:
options.output.devtoolModuleFilenameTemplate,
namespace: options.output.devtoolNamespace,
}).apply(compiler);
}
}
// 取自 lib/compiler.js
// 在compile函数中出现的钩子有:beforeCompile --> compile --> make --> afterCompile
compile(callback) {
const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {
// ...
this.hooks.compile.call(params);
// compilation记录本次编译作业的环境信息
const compilation = this.newCompilation(params);
// Step5:触发make钩子,这时候会触发EntryPlugin调用addEntry,找到入口
// 其中make就是我们关心的编译过程。在这里它仅是一个钩子触发,真正的编译执行是注册在这个钩子的回调上面(封装在各种插件里面的逻辑)
this.hooks.make.callAsync(compilation, err => {
// ...
this.hooks.finishMake.callAsync(compilation, err => {
// ...
process.nextTick(() => {
compilation.finish(err => {
// 封装构建结果(seal),逐次对每个module和chunk进行整理,每个chunk对应一个入口文件
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {
// 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,
// 不然运行流程将会一直卡在这不往下执行
return callback(null, compilation)
});
});
});
});
});
});
});
}
调用 this.addEntry 函数表示正式开始触发构建内容。
在 Step5 触发了this.hooks.make.callAsync
后,会触发 EntryPlugin 调用 addEntry,再进一步触发以下逻辑:
this.addEntry --> this._addEntryItem -> this.addModuleTree --> this.handleModuleCreation --> this.factorizeModule --> this.addModule --> this.buildModule --> this._buildModule --> module.build...
# 构建阶段(make)
# Step1:编译模块
进入 make 阶段,即触发 compilation.hooks.make 钩子,从 entry 出发开始编译模块,其中会根据模块资源类型调用对应的 loader 进行转译,然后生成 AST,再遍历 AST 去收集依赖。
根据模块间的引用关系,逐步递归构建出模块依赖关系图(ModuleDependencyGraph),依赖关系图表达了模块与模块之间互相引用的先后次序。
# Step2:处理依赖关系图,完成模块编译。
上一步递归处理所有能触达到的模块后,得到了每个模块被翻译后的内容以及它们之间的依赖关系图。
基于依赖关系图, webpack 就可以推断出模块运行之前需要先执行那些依赖模块,也就可以进一步推断出哪些模块应该打包在一起,哪些模块可以延后加载(异步执行)。
# 细节展开
构建过程引入来自文杰大佬文章 (opens new window)的图和文字描述:
Dependency
:在模块中引用其它模块,例如 import "a.js" 语句,webpack 会先将引用关系表述为 Dependency 子类并关联 module 对象,等到当前 module 内容都解析完毕之后,启动下次循环开始,将 Dependency 对象转换为适当的 Module 子类。
ModuleGraph
对象通过_dependencyMap
属性记录Dependency
对象与ModuleGraphConnection(originModule/module)
连接对象之间的映射关系,后续的处理中可以基于这层映射迅速找到 Dependency 实例对应的引用与被引用者
ModuleGraph
对象通过_moduleMap
在module
基础上附加ModuleGraphModule
信息,而ModuleGraphModule(incomingConnections表示谁引用了自己/outgoingConnections表示自己引用了谁)
最大的作用就是记录了模块的引用与被引用关系,后续的处理可以基于该属性找到 module 实例的所有依赖与被依赖关系
依赖关系收集过程主要发生在两个节点:
addDependency
和handleModuleCreation
:
addDependency
: webpack 从模块内容中解析出引用关系后,创建适当的 Dependency 子类并调用该方法记录到 module 实例
handleModuleCreation
:根据文件类型构建 module 子类
# 例子
假设,有入口文件index.js
,依赖a.js
和b.js
,其中a.js
依赖c.js
和d.js
,那解析的过程可以分为以下步骤:
Step1: EntryPlugin 根据 entry 配置找到 index.js 文件,调用
compilation.addEntry
函数触发构建流程;Step2: 触发
handleModuleCreation
,通过module.build
对 module 进行编译;module.build
会生成了一个module[index.js]
,及其依赖对象列表dependence[a.js]
和dependence[b.js]
;其中步骤包括:
1)会调用runLoaders
转译 module 源文件内容,通常是从各类资源类型转译为 JavaScript 代码;(原因是 webpack 只识别 JS 模块,其他格式需要 loader 协助)2)调用
acorn
将 JS 文本解析为AST
;相关代码 lib/NormalModule.js#L1004:
result = this.parser.parse(this._ast || this._source.source()
;this.parser
其实就是JavascriptParser的实例对象
,JavascriptParser 会调用第三方包 acorn 的 parse 方法对 JS 代码进行语法解析成AST
。(这里有可能在 loader 处理过程中就直接将文件转成 AST 了,这种情况会被保存到extraInfo.webpackAST
中,然后在这一步就可直接复用,以避免重复生成 AST,提升性能。)3)遍历 AST,触发各种钩子,其中包括:
HarmonyImportDependencyParserPlugin 插件监听
import
和importSpecifier
钩子,对依赖资源进行逻辑处理;遇到 import/require 语句就增加相关依赖,调用 module 对象的
addDependency
将依赖对象加入到 module 依赖列表中。
接下来就是对依赖列表进行处理。
Step3: AST 遍历完,会回到 dobuild 回调,调用 handleParseResult。对于 module 新增的依赖再次调用
handleModuleCreation
,控制流回到 Step2。调用
handleModuleCreation
方法创建 Dependency 对应的子模块对象,之后调用moduleGraph.setResolvedModule
将父子引用信息记录到moduleGraph
对象上。module[index.js]
的 handleParseResult 函数,继续处理 a.js、b.js 文件,递归上述流程,进一步得到 a、b 模块module[a.js]
和module[b.js]
; 接着,对module[a.js]
也会进行收集依赖,得到其依赖对象列表dependence[c.js]
和dependence[d.js]
。
这个过程中,数据流module => ast => dependencies => module
,即先转 AST 再从中遍历找依赖。经过收集后的 ModuleGraph 结构:
ModuleGraph: {
_dependencyMap: Map(3){
{
EntryDependency{request: "./src/index.js"} => ModuleGraphConnection{
module: NormalModule{request: "./src/index.js"},
originModule: null // 入口模块没有引用者,故设置为 null
}
},
{
HarmonyImportSideEffectDependency{request: "./src/a.js"} => ModuleGraphConnection{
module: NormalModule{request: "./src/a.js"},
originModule: NormalModule{request: "./src/index.js"}
}
},
// b.js同理...
{
HarmonyImportSideEffectDependency{request: "./src/c.js"} => ModuleGraphConnection{
module: NormalModule{request: "./src/c.js"},
originModule: NormalModule{request: "./src/a.js"}
}
},
// d.js同理...
},
_moduleMap: Map(3){
NormalModule{request: "./src/index.js"} => ModuleGraphModule{
incomingConnections: Set(1) [ // 谁引用了自己
// entry 模块,对应 originModule 为null
ModuleGraphConnection{ module: NormalModule{request: "./src/index.js"}, originModule:null }
],
outgoingConnections: Set(2) [ // 自己引用了谁
// 从 index 指向 a 模块
ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} },
// 从 index 指向 b 模块
ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} }
]
},
NormalModule{request: "./src/a.js"} => ModuleGraphModule{
incomingConnections: Set(1) [
ModuleGraphConnection{ module: NormalModule{request: "./src/a.js"}, originModule: NormalModule{request: "./src/index.js"} }
],
outgoingConnections: Set(2) [
// 从 a 指向 c 模块
ModuleGraphConnection{ module: NormalModule{request: "./src/c.js"}, originModule: NormalModule{request: "./src/a.js"} },
// 从 a 指向 d 模块
ModuleGraphConnection{ module: NormalModule{request: "./src/d.js"}, originModule: NormalModule{request: "./src/a.js"} }
]
},
},
NormalModule{request: "./src/b.js"} => ModuleGraphModule{
incomingConnections: Set(1) [
ModuleGraphConnection{ module: NormalModule{request: "./src/b.js"}, originModule: NormalModule{request: "./src/index.js"} }
],
// b 模块没有其他依赖,故 outgoingConnections 属性值为 undefined
outgoingConnections: undefined
},
NormalModule{request: "./src/c.js"} => ModuleGraphModule{
incomingConnections: Set(1) [
ModuleGraphConnection{ module: NormalModule{request: "./src/c.js"}, originModule: NormalModule{request: "./src/a.js"} }
],
outgoingConnections: undefined
},
// d.js同理...
}
}
# 生成阶段(seal)
至此,从入口文件开始,webpack 已经收集完整了模块的信息和依赖项,接下来就是如何进一步打包封装模块了。构建阶段围绕 module 展开,生成阶段则围绕 chunks 展开。
chunks 生成的大概过程:
1.webpack 首先会将 entry 中对应的 module 都生成一个新的 chunk。
2.遍历 module 对应的依赖列表,将其依赖的模块也加入到 chunk 中。
3.如果依赖的模块是动态引入的模块,会根据这个 module 创建一个新的 chunk,继续遍历依赖。
4.重复上面的过程,直至得到所有的 chunk。
# seal 过程
seal 主要完成从 module 到 chunks 的转化,主要围绕 Chunk 及 ChunkGroup 两个类型展开。
1)chunk,由 module 组成,一个 chunk 可以包含多个 module,它是 webpack 编译打包后输出的最终文件;
2)chunkGroup,由 chunk 组成,一个 chunkGroup 可以包含多个 chunk,在生成/优化 ChunkGraph 时会用到。
Step1:根据 moduleGraph 构建本次编译的 chunkGraph 对象;
Step2:遍历 compilation.modules 集合,对 entry 入口文件 和动态引入分配给不同的 Chunk 对象(生成 chunk);
Step3:createChunkAssets 遍历 module/chunk,把所有依赖项通过对应的模板 render 出一个拼接好的字符串;
createChunkAssets 执行过程中,会优先读取 cache 中是否已经有了相同 hash 的资源,如果有,则直接返回内容,否则才会继续执行模块生成的逻辑,并存入 cache 中。
seal 阶段经历了很多的优化,最终生成的代码会存放在 Compilation 的 assets 属性上。第三方 webpack 插件也很多会通过 compilation.assets 和 compilation.chunks 进行逻辑拓展。
// 取自 lib/Compilation.js#L2235
// 根据moduleGraph构建ChunkGraph
const chunkGraph = new ChunkGraph(this.moduleGraph);
// ChunkGraph过程中会调用_getGraphRoots方法,通过 moduleGraph.getOutgoingConnections(module)查找 module 实例的所有依赖
this.chunkGraph = chunkGraph;
for (const module of this.modules) {
ChunkGraph.setChunkGraphForModule(module, chunkGraph);
}
// 遍历根据 addEntry 方法中收集到入口文件组成的this.entries数组
const chunkGraphInit = new Map();
for (const [name, { dependencies, includeDependencies, options }] of this.entries) {
// 每个入口 创建一个chunk
const chunk = this.addChunk(name);
if (options.filename) {
chunk.filenameTemplate = options.filename;
}
// 每一个 entryPoint 就是一个 chunkGroup
const entrypoint = new Entrypoint(options);
if (!options.dependOn && !options.runtime) {
// 设置 runtime chunk
// 同时内部会有个比较特殊的 runtimeChunk(当 webpack 最终编译完成后包含的webpack runtime代码最终会注入到 runtimeChunk当中)
entrypoint.setRuntimeChunk(chunk);
}
entrypoint.setEntrypointChunk(chunk);
// 设置 chunkGroups 的内容
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);
// 建立起 chunkGroup 和 chunk 之间的关系
connectChunkGroupAndChunk(entrypoint, chunk)
for (const dep of [...this.globalEntry.dependencies, ...dependencies]) {
entrypoint.addOrigin(null, { name }, /** @type {any} */ (dep).request);
const module = this.moduleGraph.getModule(dep);
if (module) {
chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
const modulesList = chunkGraphInit.get(entrypoint);
if (modulesList === undefined) {
// 构建了chunk 和入口 module 之间的联系
chunkGrphInit.set(entrypoint, [module]);
} else {
modulesList.push(module);
}
}
}
// ...
// 取自 lib/Compilation.js#L3763
createChunkAssets(callback){
const outputOptions = this.outputOptions;
const cachedSourceMap = new WeakMap();
/** @type {Map<string, {hash: string, source: Source, chunk: Chunk}>} */
const alreadyWrittenFiles = new Map();
asyncLib.forEach(
this.chunks,
(chunk, callback) => {
// manifest是数组结构,每个manifest元素都提供了 `render` 方法提供后续的源码字符串生成服务。
// 至于render方法何时初始化的,在`./lib/MainTemplate.js`中
let manifest = this.getRenderManifest()
asyncLib.forEach(manifest,(fileManifest, callback) => {
...
source = fileManifest.render()
this.emitAsset(file, source, assetInfo)
},
callback
)
},
callback
)
}
// todo 未完待续,回头继续分析
# 写入阶段(emit)
在 seal 执行后,关于模块所有信息以及打包后源码信息都存在内存中,也可以在传入事件回调的 compilation.assets 上拿到所需数据。
此时控制流会回到 compiler 中,触发 emitAssets。函数内部调用 compiler.outputFileSystem.writeFile
方法,将 assets 集合根据 output 配置的 path 属性,将文件输出到指定的文件夹。